﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Migrator.Packets;
using Migrator.Util;
using System.Diagnostics;
using System.Drawing;
using Migrator.Services.DHCP.Packets;
using Migrator.Services.DHCP;
using System.Reflection;
using Migrator.Exceptions;
using Migrator.Core;
using Migrator.Services;
using Migrator.Services.Advertisement;
using Migrator.Services.MobileAgents;
using Migrator.Services.RouteOptimization;

namespace Migrator.Entities
{
    /// <summary>
    /// Represents a router.
    /// </summary>
    public class Router : Node
    {
        #region Constructors
        public Router(MACAddress macAddress, IPAddress address, Router defaultRouter, PointF location)
            : base(macAddress, address, defaultRouter, location)
        {
            this.NetworkAddress = new IPAddress(address.GetAddressBytes().First(), 0, 0, 0);

            // Create the DHCP server
            this.services.Add(new DHCPServer(this).Configure(10, 200));

            // Mobile IP services
            IService homeAgent = new HomeAgent(this).Configure();
            IService foreignAgent = new ForeignAgent(this).Configure();
            IService advertiser = new Advertiser(this).Configure(homeAgent, foreignAgent);
            this.services.AddRange(new List<IService>() { homeAgent, foreignAgent, advertiser });

            // Route optimization service
            this.services.Add(new RouteOptimizationService(this).Configure());
        }
        #endregion

        #region Public methods
        public override void Update()
        {
            this.ProcessReceivedPackets();

            // Update each service
            foreach (var service in this.services)
            {
                if (service.Enabled)
                    service.Update();
            }

            // Update each connected node
            foreach (Node node in nodes.Values)
            {
                node.Update();
            }

            // Process deregistration requests
            while (nodesToRemove.Count > 0)
            {
                this.nodes.Remove(nodesToRemove.Dequeue());
            }
        }

        /// <summary>
        /// Sends a packet to its destination.
        /// </summary>
        /// <param name="packet"></param>
        public override void Send(IPPacket packet)
        {
            // If the packet is not for this network, forward it to the
            // default router
            if (!packet.DestinationIP.IsInNetwork(this.NetworkAddress))
            {
                base.Send(packet);
            }
            else
            {
                // Otherwise, forward it to the correct node
                if (this.Contains(packet.DestinationIP))
                {
                    this.SendLinkLayer(packet, this[packet.DestinationIP].MACAddress);
                }
                else
                {
                    // Packet won't be sent, but create the event anyway.
                    // Maybe this shouldn't be here...
                    this.OnBeforePacketSent(packet);
                }
            }
        }

        /// <summary>
        /// Registers a new node in this router's network
        /// </summary>
        /// <param name="node">The node to register</param>
        public void RegisterNode(Node node)
        {
            this.nodes.Add(node.MACAddress, node);
        }

        /// <summary>
        /// Deregisters a node from this router's network
        /// </summary>
        /// <param name="node">The node to deregister</param>
        public void DeregisterNode(Node node)
        {
            this.nodesToRemove.Enqueue(node.MACAddress);
        }

        /// <summary>
        /// Iterates over all the nodes connected to the tree whose root
        /// is this router, applying the function applyNode() to each one,
        /// and returning the result in a list. Just like LINQ's Select()
        /// </summary>
        /// <typeparam name="T">Type of the resulting List</typeparam>
        /// <param name="applyNode">The function to apply to each node</param>
        /// <returns>The list with the result of the projection</returns>
        public IEnumerable<T> NetworkSelect<T>(Func<Node, T> applyNode)
        {
            return this.nodes.Values.SelectMany(node =>
                {
                    var result = new List<T>();

                    // If it's a router, recursive call
                    if (node is Router)
                        result.AddRange(((Router)node).NetworkSelect(applyNode));

                    // Otherwise, add the result of the projection to the result
                    result.Add(applyNode(node)); 
             
                    return result;
                }
                );
        }

        /// <summary>
        /// Returns all the nodes in range of the given location
        /// </summary>
        /// <param name="location">The location</param>
        /// <returns>The list with all the nodes in range</returns>
        public IEnumerable<Node> GetNodesInRange(PointF location)
        {
            // Yes, this is sweet...
            return this.NetworkSelect(node => node).Where(node => node.Location.DistanceTo(location) <= node.Radius);
        }

        /// <summary>
        /// Checks if this router contains the node with the given MAC address
        /// </summary>
        /// <param name="macAddress">The MAC address to check</param>
        /// <returns>true if the node was found, false otherwise</returns>
        public bool Contains(MACAddress macAddress)
        {
            return this.NetworkSelect(node => node.MACAddress).Where(mac => mac == macAddress).Count() != 0;
        }

        /// <summary>
        /// Checks if this router contains the node with the given IP address
        /// </summary>
        /// <param name="address">The IP address to check</param>
        /// <returns>true if the node was found, false otherwise</returns>
        public bool Contains(IPAddress address)
        {
            return this.NetworkSelect(node => node.IPAddress).Where(ip => ip == address).Count() != 0;
        }

        #endregion

        #region Public properties

        /// <summary>
        /// Network's identifier address (e.g. 192.168.1.0)
        /// </summary>
        public IPAddress NetworkAddress
        {
            get;
            protected set;
        }

        /// <summary>
        /// Returns the node with the given IP Address
        /// <remarks>Checks the whole tree, not just the directly connected nodes.</remarks>
        /// </summary>
        /// <param name="address">The IP address of the desired node.</param>
        /// <returns>The node with the given IP address</returns>
        public Node this[IPAddress address]
        {
            get { return this.FindNode(address); }
        }

        /// <summary>
        /// Returns the node with the given MAC Address
        /// <remarks>Checks the whole tree, not just the directly connected nodes.</remarks>
        /// </summary>
        /// <param name="address">The MAC address of the desired node.</param>
        /// <returns>The node with the given MAC address</returns>
        public Node this[MACAddress address]
        {
            get { return this.FindNode(address); }
        } 

        #endregion

        #region Internal methods

        /// <summary>
        /// Process incoming messages
        /// <remarks>
        /// In order to process messages, a router needs to check the destination
        /// of the message. If the message is not destinated to the router, it
        /// forwards it.
        /// Also, broadcast messages are not re-sent.
        /// </remarks>
        /// </summary>
        /// <param name="message"></param>
        protected override void ProcessPacket(IPPacket packet)
        {
            Debug.WriteLine(">>> " + this.ToString() + " received " + packet);
            base.ProcessPacket(packet);

            // If the packet is not for me, send it to the destination
            // Note that broadcast messages are not broadcasted by a normal router
            if (packet.DestinationIP != this.IPAddress && packet.DestinationIP != IPAddress.BroadcastAddress)
            {
                this.Send(packet);
            }
        }

        /// <summary>
        /// Returns the node with the given MAC address
        /// </summary>
        /// <param name="address">The IP address of the desired node</param>
        /// <returns>The node with the given IP address</returns>
        protected Node FindNode(MACAddress address)
        {
            if (address == this.MACAddress)
                return this;

            Node node = this.NetworkSelect(n => n).Where(n => n.MACAddress == address).FirstOrDefault();
            if (node != null)
            {
                return node;
            }
            else
            {
                throw new UnknownNodeException(address);
            }
        }

        /// <summary>
        /// Returns the node with the given IP address
        /// </summary>
        /// <param name="address">The IP address of the desired node</param>
        /// <returns>The node with the given IP address</returns>
        protected Node FindNode(IPAddress address)
        {
            if (address == this.IPAddress)
                return this;

            Node node = this.NetworkSelect(n => n).Where(n => n.IPAddress == address).FirstOrDefault();
            if (node != null)
            {
                return node;
            }
            else
            {
                throw new UnknownNodeException(address);
            }
        }
        
        #endregion

        #region Internal data
        /// <summary>
        /// Nodes connected to the router's network.
        /// </summary>
        protected Dictionary<MACAddress, Node> nodes = new Dictionary<MACAddress, Node>();

        /// <summary>
        /// List of nodes to deregister.
        /// <remarks>This is necessary because we can't remove nodes while
        /// we are iterating over them. So, this queue is processed after
        /// that.
        /// </remarks>
        /// </summary>
        protected Queue<MACAddress> nodesToRemove = new Queue<MACAddress>();

        #endregion
    }
}
